Micron Document
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| SparkN0de-git | SparkN0de |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


Commit a042f02cd20d7c6a62537c48e43f5ab1117da008


Parents : a34bfd7
Author : Ivan <ivan@quad4.io>
Signature : Invalid signer <e46112d44649266d71fe2193e00a4710>, author is <ivan@quad4.io>
Date : 2026-07-04T15:50:59-05:00

feat(meshchat): add interface stats retrieval method and update endpoint to use it, improving error handling and logging

Changes
Diff

diff --git a/meshchatx/meshchat.py b/meshchatx/meshchat.py
index 8da1dfc7..e1bf8a5c 100644
--- a/meshchatx/meshchat.py
+++ b/meshchatx/meshchat.py
@@ -2247,6 +2247,24 @@ class ReticulumMeshChat:
return [ReticulumMeshChat._to_jsonable(v) for v in obj]
return obj
+ def _get_interface_stats_payload(self) -> dict:
+ empty: dict = {"interfaces": []}
+ reticulum = getattr(self, "reticulum", None)
+ if not reticulum:
+ return empty
+ try:
+ raw = reticulum.get_interface_stats()
+ except Exception as exc:
+ logger.warning("Failed to get interface stats: %s", exc)
+ return empty
+ if not isinstance(raw, dict):
+ return empty
+ payload = self._to_jsonable(raw)
+ for interface in payload.get("interfaces") or []:
+ if isinstance(interface, dict) and "short_name" in interface:
+ interface["interface_name"] = interface["short_name"]
+ return payload
+
@staticmethod
def discovery_filter_candidates(interface):
if not isinstance(interface, dict):
@@ -7612,67 +7630,53 @@ class ReticulumMeshChat:
if len(interfaces) > max_disc:
interfaces = interfaces[:max_disc]
active = []
- try:
- if hasattr(self, "reticulum") and self.reticulum:
- stats = self.reticulum.get_interface_stats().get(
- "interfaces",
- [],
- )
- active = []
- for s in stats:
- name = s.get("name") or ""
- parsed_host = None
- parsed_port = None
- if "/" in name:
+ stats = self._get_interface_stats_payload().get("interfaces", [])
+ for s in stats:
+ name = s.get("name") or ""
+ parsed_host = None
+ parsed_port = None
+ if "/" in name:
+ try:
+ host_port = name.split("/")[-1].strip("[]")
+ if ":" in host_port:
+ parsed_host, parsed_port = host_port.rsplit(
+ ":",
+ 1,
+ )
try:
- host_port = name.split("/")[-1].strip("[]")
- if ":" in host_port:
- parsed_host, parsed_port = host_port.rsplit(
- ":",
- 1,
- )
- try:
- parsed_port = int(parsed_port)
- except Exception:
- parsed_port = None
- else:
- parsed_host = host_port
+ parsed_port = int(parsed_port)
except Exception:
- parsed_host = None
parsed_port = None
+ else:
+ parsed_host = host_port
+ except Exception:
+ parsed_host = None
+ parsed_port = None
- host = (
- s.get("target_host") or s.get("remote") or parsed_host
- )
- port = (
- s.get("target_port")
- or s.get("listen_port")
- or parsed_port
- )
- transport_id = s.get("transport_id")
- if isinstance(transport_id, (bytes, bytearray)):
- transport_id = transport_id.hex()
+ host = s.get("target_host") or s.get("remote") or parsed_host
+ port = s.get("target_port") or s.get("listen_port") or parsed_port
+ transport_id = s.get("transport_id")
+ if isinstance(transport_id, (bytes, bytearray)):
+ transport_id = transport_id.hex()
- active.append(
- {
- "name": name,
- "short_name": s.get("short_name"),
- "type": s.get("type"),
- "target_host": host,
- "target_port": port,
- "listen_ip": s.get("listen_ip"),
- "connected": s.get("connected"),
- "online": s.get("online"),
- "status": s.get("status"),
- "transport_id": transport_id,
- "network_id": s.get("network_id"),
- "autoconnect_source": s.get("autoconnect_source"),
- "txb": s.get("txb"),
- "rxb": s.get("rxb"),
- },
- )
- except Exception as e:
- logger.debug(f"Failed to get interface stats: {e}")
+ active.append(
+ {
+ "name": name,
+ "short_name": s.get("short_name"),
+ "type": s.get("type"),
+ "target_host": host,
+ "target_port": port,
+ "listen_ip": s.get("listen_ip"),
+ "connected": s.get("connected"),
+ "online": s.get("online"),
+ "status": s.get("status"),
+ "transport_id": transport_id,
+ "network_id": s.get("network_id"),
+ "autoconnect_source": s.get("autoconnect_source"),
+ "txb": s.get("txb"),
+ "rxb": s.get("rxb"),
+ },
+ )
if len(active) > max_disc:
active = active[:max_disc]
@@ -11728,24 +11732,9 @@ class ReticulumMeshChat:
# get interface stats
@routes.get("/api/v1/interface-stats")
async def interface_stats(request):
- interface_stats = {"interfaces": []}
- if hasattr(self, "reticulum") and self.reticulum:
- try:
- raw = self.reticulum.get_interface_stats()
- if isinstance(raw, dict):
- interface_stats = self._to_jsonable(raw)
- for interface in interface_stats.get("interfaces") or []:
- if (
- isinstance(interface, dict)
- and "short_name" in interface
- ):
- interface["interface_name"] = interface["short_name"]
- except Exception:
- pass
-
return web.json_response(
{
- "interface_stats": interface_stats,
+ "interface_stats": self._get_interface_stats_payload(),
},
)

diff --git a/meshchatx/src/backend/landlock_sandbox.py b/meshchatx/src/backend/landlock_sandbox.py
index bbef9fb8..18910702 100644
--- a/meshchatx/src/backend/landlock_sandbox.py
+++ b/meshchatx/src/backend/landlock_sandbox.py
@@ -221,6 +221,8 @@ def _collect_read_roots() -> list[str]:
"/etc",
"/bin",
"/sbin",
+ # RNS get_interface_stats() uses psutil for rss; psutil reads /proc/self.
+ "/proc",
}
for path in sys.path:
existing = _existing_dir(path)

diff --git a/tests/backend/test_interface_stats_endpoint.py b/tests/backend/test_interface_stats_endpoint.py
index 6965f096..e6db2fb0 100644
--- a/tests/backend/test_interface_stats_endpoint.py
+++ b/tests/backend/test_interface_stats_endpoint.py
@@ -90,3 +90,30 @@ async def test_interface_stats_serializes_bytes_and_parent_hash_null(
assert stats["interfaces"][0]["parent_interface_hash"] is None
assert stats["interfaces"][0]["hash"] == ("01" * 16)
assert stats["interfaces"][1]["short_name"] == "Main"
+
+
+@pytest.mark.asyncio
+async def test_get_interface_stats_payload_logs_on_failure(
+ mock_rns_minimal, temp_dir, caplog
+):
+ import logging
+
+ with patch("meshchatx.meshchat.generate_ssl_certificate"):
+ app_instance = ReticulumMeshChat(
+ identity=mock_rns_minimal,
+ storage_dir=temp_dir,
+ reticulum_config_dir=temp_dir,
+ )
+ app_instance.reticulum = MagicMock()
+ app_instance.reticulum.get_interface_stats.side_effect = OSError(
+ "Landlock: access denied"
+ )
+
+ with caplog.at_level(logging.WARNING):
+ payload = app_instance._get_interface_stats_payload()
+
+ assert payload == {"interfaces": []}
+ assert any(
+ "Failed to get interface stats" in record.message
+ for record in caplog.records
+ )

diff --git a/tests/backend/test_landlock_sandbox.py b/tests/backend/test_landlock_sandbox.py
index 5dc9c7e8..868b1d94 100644
--- a/tests/backend/test_landlock_sandbox.py
+++ b/tests/backend/test_landlock_sandbox.py
@@ -75,3 +75,8 @@ def test_landlock_kernel_supported_on_linux():
ll._landlock_support_cached = None
supported = ll.landlock_kernel_supported()
assert isinstance(supported, bool)
+
+
+def test_collect_read_roots_includes_proc_for_psutil():
+ roots = ll._collect_read_roots()
+ assert "/proc" in roots


──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────